#include<iostream>
/**
 * 头文件不一定要跟它对应功能实现的app放在一起，
 * 因为只需要执行编译命令时写-I../includeDirPath即可，这个命令会在当前目录下创建一个映射，
 * 就如同头文件和使用它的函数在同一个目录中
 * -------------
 * bin目录下编译出来的文件和头文件以及头文件的实现函数已经没有关系了，bin目录移至本机的任何位置都可以运行
 * -------------
 * 运行:
 * ./my_app -h 127.0.0.1 -p 8080 >my_app_run.log 2>&1 &
 */
#include "ArgException.h"
#include "cfg.h"
#include "mymath.h"
#include"mysqlutili.h"
#define FILE_PATH "db.conf"
// About Socket, reference blog link is https://blog.csdn.net/bandaoyu/article/details/83312254
/**
 * 关于方法setsockopt解决“ close(serv_sock)后立马重启报地址重用（bind server port error, port already in use）
 * 参照博客https://blog.csdn.net/LF_2016/article/details/61198176
 */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
// base params
#define FAIL_STATUS_CODE -1
#define DONE_STATUS_CODE 0
#define BUFFSIZE 4096
int count_client_connect = 0;
#define MAX_CLI_CONN 2
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
using namespace std;
// 每个线程用到的参数，结构体
struct pthread_args
{
	// 客户端的描述符
	int* cli_describe_symbol;
	// 该结构体的type_参数用于指明该线程的用途类型，如type_ = 'recv' 或 'send'
	string type_;
	// 客户端的ip
	char *client_ip;
	// 当前线程
	pthread_t thrd;
};
// 接收和发送消息的方法，用于线程【选择响应和退出】
/**
	致已有Console App编程经验的初学者，可作以下拓展：
		服务端可以通过客户端的入参，判断是否该
		1.(1)若只查询数据库，提示客户输入operate_type=(1)后，执行客户端发送的sql语句，成功执行则返回结果，失败执行则返回空值，仅一次
		2.(2)若需要与客户端进行交互，在提示客户输入operate_type=(2)后，服务端使用scanf("%s", s)与客户端动态输入聊天，仅一次
		3.(3)长久查询数据库模式，后续每次客户端的消息都用作sql指令去查询数据库
		4.(4).长久聊天模式
		5.(5)根据客户端发送‘quit’指令，可以断开与客户端的连接
	-- 当前只实现了(3)的模式。客户端当前使用telnet工具。
*/
Config cfg(FILE_PATH);
MYSQLUTILI mud(cfg.getCFG("host").c_str(),
			stoi(cfg.getCFG("port")),
			cfg.getCFG("user").c_str(),
			cfg.getCFG("pass").c_str(),
			cfg.getCFG("db").c_str(),NULL,0);
void* thread_recv_send(void *arg)
{
	char buf[BUFFSIZE];
	int recv_len;
	// 取出参数列表的参数，使用指针取值并赋值给新变量
	struct pthread_args *pt = (struct pthread_args *) arg;
	int* c_d_i = pt->cli_describe_symbol;
	pthread_t thrd = pt->thrd;
	// 相应客户端参数
	string res;
	// 轮询，保持与客户端的长连接
	while (1)
	{
		memset(buf, 0, sizeof(buf));
		// 接收客户端消息
		if((recv_len= recv(*c_d_i, buf, sizeof(buf), 0) )>0){
			buf[recv_len] = '\0';
			// 增加socket断联
			if ( strncmp("exit",buf,4) == 0 ) break;
			res = mud.executeForResult(buf);
			//向客户端发送响应数据
			send(*c_d_i, (const void *)res.c_str(),res.length(),0);
		}
		else{
			break;
		}
	}
	pthread_cancel(thrd);
	pthread_mutex_lock(&mutex);
	count_client_connect--;
	printf("socket closed!");
	pthread_mutex_unlock(&mutex);
	close(*c_d_i);
	return NULL;
}

int main(int argc, char **argv) {
	// 一、通过启动参数设置[$ ./my_app -h 192.168.65.160 -p 8080 ]
	char const *host = "127.0.0.1";
	int port = 1234;
	if ( (argc-1)%2 != 0 ){
		try{
			throw ArgException();
		}catch(ArgException e){
			printf("%s\n",e.what());
			return FAIL_STATUS_CODE;
		}
	}else{
		string tmp_, tmp_v, _h, _p;
		_h = "-h";
		_p = "-p";
		for (int i = 1; i < argc; i+=2) {
			tmp_ = argv[i];
			// 字符串比较，相等为1，不等为0
			if ( (tmp_ == _h) == 1 ){
				host = argv[i+1];
			}
			if ( (tmp_ == _p) == 1 ){
				tmp_v = argv[i+1];
				port = stoi(tmp_v);
				// char*数组转int，可以不通过string中转，如：int port = atoi(argv[i+1]);
			}
		}
	}
	// 二、创建server端监听某一个端口号并用于提供服务
	// (1).创建套接字
	int serv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	// 服务端声明
	/*
		四大整数类型byte、short、int、long分别占1，2，4，8字节，分别能代表8，16，32，64位的二进制数。
		如192.168.160.53=>11000000 10101000 10100000 00110101，则int xxx = 3232276533
	*/
	struct sockaddr_in serv_addr;
	// (2).定义IP/PORT等参数
	memset(&serv_addr, 0, sizeof(serv_addr)); // 内存块初始化，将serv_addr的前sizeof位上的值清0，及当前分配的内存块区域值全部清零
	serv_addr.sin_family = AF_INET;  //1.使用IPv4地址
	// 2.监听本机的哪个ip，过去是由程序做决定，后续就由启动参数做决定
	if ( strcmp("localhost",host)==1 ) {
		serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 针对于主机有多块网卡的情况
	}else {
		serv_addr.sin_addr.s_addr = inet_addr(host); // 绑定本机某一网卡的ip
	}
	serv_addr.sin_port = htons(port);  //3.绑定指定的端口
	// (3).设置IP地址可重用，解决瞬时重启时的TIME_WAIT问题
	int on = 1;
	if (setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
		printf("setsockopt error:%s \n", strerror(errno));
		return FAIL_STATUS_CODE;
	}
	// (4).绑定host&端口
	if( bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1){
		printf("bind socket error:%s \n", strerror(errno));
		return FAIL_STATUS_CODE;
	}
	// (5).进入监听状态，
	if( listen(serv_sock, 20) == -1){
		printf("listen socket error:%s \n", strerror(errno));
		return FAIL_STATUS_CODE;
	}
	// 三、监听客户端请求部分，客户端声明
	// (1).客户端IP结构
    struct sockaddr_in client_addr;
    // (2).
    socklen_t len = sizeof(client_addr);
	// (3).客户端的线程
    pthread_t thrd1;
    // (4).定义装参数的结构体
	struct pthread_args pa;
    // 长轮询（polling）
	while(1){
		memset(&client_addr, 0, sizeof(client_addr));
		int* clnt_sock = (int *) malloc(sizeof(int));
		*clnt_sock = accept(serv_sock, (struct sockaddr*)&client_addr, &len);
		if (*clnt_sock == -1)
		{
			printf("accept fail %s\n", strerror(errno));
			return FAIL_STATUS_CODE;
		}
		// 设置了该应用最大响应的客户端数的静态变量，需要进行判断是否要响应客户端，线程计数只需要关注【接收线程】即可
		if (count_client_connect >= MAX_CLI_CONN){
			printf("connect have already be full! \n");
			close(*clnt_sock);
			continue;
		}
		// 设置线程参数
		pa.cli_describe_symbol = clnt_sock;
		pa.type_ = "auto mode";
		pa.client_ip = inet_ntoa(client_addr.sin_addr);
		pa.thrd = thrd1;
		if((pthread_create(&thrd1, NULL, thread_recv_send, &pa)) == -1){//客户端来一个请求就创建一个线程
			printf("create error!\n");
		}
		else{
			printf("create success!\n");
			// 对已连接的客户端进行计数，更改静态变量时要加互斥锁。到此处即代表服务端会响应客户端请求，连接数+1，当recv线程关闭时才会-1。
			pthread_mutex_lock(&mutex);
			count_client_connect++;
			pthread_mutex_unlock(&mutex);
		}
	}
	// 服务端描述符，关闭
	close(serv_sock);
	return DONE_STATUS_CODE;
}
